Skip to main content

pacenotes_installer\wizard\scene/
download.rs

1use windows_sys::Win32::Foundation::{HWND, LPARAM, WPARAM};
2use windows_sys::Win32::UI::Controls as ctl;
3use windows_sys::Win32::UI::WindowsAndMessaging as wm;
4
5/// Scene 4: the catalogue as a tree, checked by what the user wants this run.
6pub unsafe extern "system" fn download_page(
7    dialog: HWND,
8    message: u32,
9    wparam: WPARAM,
10    lparam: LPARAM,
11) -> isize {
12    match message {
13        wm::WM_INITDIALOG => {
14            crate::wizard::page::adopt_state(dialog, lparam);
15            super::download_tree_view::enable_partial_checkboxes(dialog);
16            1
17        }
18        crate::worker::WM_WORKER_DONE => {
19            on_worker_done(dialog, wparam, lparam);
20            1
21        }
22        super::download_tree_view::WM_SYNC_CHECKS => {
23            if let Some(state) = crate::wizard::page::state(dialog) {
24                super::download_tree_view::refresh_checks(dialog, state);
25            }
26            1
27        }
28        wm::WM_NOTIFY => match crate::wizard::page::notify_code(lparam) {
29            Some(ctl::PSN_SETACTIVE) => {
30                crate::wizard::page::set_sheet_caption(dialog);
31                crate::wizard::page::set_header(dialog, crate::i18n::IDS_S4_TITLE);
32                crate::wizard::page::set_buttons(dialog, ctl::PSWIZB_BACK | ctl::PSWIZB_NEXT);
33                crate::wizard::page::set_next_text(dialog, crate::i18n::IDS_BTN_DOWNLOAD);
34                start_catalogue_fetch(dialog);
35                crate::wizard::page::reply(dialog, 0)
36            }
37            Some(super::download_tree_view::TVN_ITEMCHANGEDW) => {
38                super::download_tree_view::on_item_changed(dialog, lparam)
39            }
40            Some(ctl::PSN_WIZNEXT) => on_download(dialog),
41            Some(ctl::PSN_QUERYCANCEL) => crate::wizard::page::query_cancel(dialog),
42            _ => 0,
43        },
44        _ => 0,
45    }
46}
47
48/// A failed fetch is not fatal: the tree falls back to what is already cached.
49unsafe fn on_worker_done(dialog: HWND, wparam: WPARAM, lparam: LPARAM) {
50    let Some(message) = crate::worker::take(lparam) else {
51        return;
52    };
53    let Some(state) = crate::wizard::page::state(dialog) else {
54        return;
55    };
56    if !state.is_current_request(wparam) {
57        return;
58    }
59    if let crate::worker::Message::Packages(result) = message {
60        match result {
61            Ok(manifest) => {
62                if state.online == Some(false) {
63                    state
64                        .log
65                        .line("the catalogue answered although /health had not");
66                }
67                state.schema_mismatch = schema_mismatch(state, &manifest);
68                build_tree(dialog, state, manifest.packages, None);
69            }
70            Err(reason) => {
71                // The only diagnostic a user can send back, though the scene recovers.
72                state
73                    .log
74                    .line(format!("the catalogue could not be read: {reason}"));
75                // Two failures in a row read as the server being down, one as the catalogue failing.
76                let status = if state.online == Some(false) {
77                    crate::i18n::IDS_S4_OFFLINE
78                } else {
79                    crate::i18n::IDS_S4_FETCH_FAILED
80                };
81                build_tree(dialog, state, Vec::new(), Some(status));
82            }
83        }
84    }
85}
86
87/// Checked only on a manifest the server sent, so an offline run is never
88/// told it is out of date; the reason goes to the run log.
89fn schema_mismatch(
90    state: &mut crate::wizard::state::WizardState,
91    manifest: &pacenotes_shared::Manifest,
92) -> bool {
93    let game = state.game_id();
94    match pacenotes_installer::schema::check(game, manifest.schemas.get(game)) {
95        Ok(()) => false,
96        Err(reason) => {
97            state.log.line(describe_schema_mismatch(game, &reason));
98            true
99        }
100    }
101}
102
103/// The modal is short; this is what the user can send back.
104fn describe_schema_mismatch(
105    game: &str,
106    reason: &pacenotes_installer::schema::Incompatible,
107) -> String {
108    match reason {
109        pacenotes_installer::schema::Incompatible::NoSchemaPublished => format!(
110            "the server published no install schema for {game}, so this installer cannot \
111             tell whether it understands the catalogue"
112        ),
113        pacenotes_installer::schema::Incompatible::GameUnknown => {
114            format!("this installer has no install schema for {game}")
115        }
116        pacenotes_installer::schema::Incompatible::Different {
117            ours,
118            theirs,
119            unknown_categories,
120        } => {
121            let detail = if unknown_categories.is_empty() {
122                "no category is new, so this is probably only an edit to the schema file"
123                    .to_string()
124            } else {
125                format!(
126                    "it has categories this installer does not know: {}",
127                    unknown_categories.join(", ")
128                )
129            };
130            format!(
131                "the {game} catalogue uses a different install schema ({detail}). \
132                 Server {theirs}, this installer {ours}"
133            )
134        }
135    }
136}
137
138/// Once per run, and whatever `/health` said: it is a convenience for scene 2's
139/// status line, and a hiccup there, or an answer still on its way, must not
140/// hide a catalogue that would have come back (D12). A failed fetch builds the
141/// tree from the cache alone.
142unsafe fn start_catalogue_fetch(dialog: HWND) {
143    let Some(state) = crate::wizard::page::state(dialog) else {
144        return;
145    };
146    if state.catalogue_loaded {
147        // Scene 4b unticks what never arrived before sending the selection back here.
148        super::download_tree_view::refresh_checks(dialog, state);
149        return;
150    }
151    state.catalogue_loaded = true;
152
153    let Some(game) = state.game else {
154        build_tree(dialog, state, Vec::new(), Some(crate::i18n::IDS_S4_EMPTY));
155        return;
156    };
157    crate::ui::set_text(
158        dialog,
159        crate::wizard::control_ids::IDC_S4_STATUS,
160        crate::i18n::IDS_S4_LOADING,
161    );
162    let request = state.next_request();
163    let base_url = state.base_url.clone();
164    let token = state.token();
165    let game_id = game.id();
166    crate::worker::spawn(dialog, request, move || {
167        crate::worker::fetch_packages(base_url, token, game_id)
168    });
169}
170
171/// Rebuilds the model from the catalogue, the cache and the game folder's
172/// ledger, and shows it.
173unsafe fn build_tree(
174    dialog: HWND,
175    state: &mut crate::wizard::state::WizardState,
176    packages: Vec<pacenotes_shared::PackageInfo>,
177    status: Option<u16>,
178) {
179    load_tree(state, packages);
180    super::download_tree_view::populate(dialog, state);
181
182    let status = status.unwrap_or(if state.tree.is_empty() {
183        crate::i18n::IDS_S4_EMPTY
184    } else {
185        crate::i18n::IDS_S4_READY
186    });
187    crate::ui::set_text(dialog, crate::wizard::control_ids::IDC_S4_STATUS, status);
188}
189
190/// The model alone, which the Configure route needs for scene 5 though it never
191/// shows scene 4's tree control.
192pub(super) fn load_tree(
193    state: &mut crate::wizard::state::WizardState,
194    packages: Vec<pacenotes_shared::PackageInfo>,
195) {
196    let game = state.game_id();
197    let cached = state.cache.entries();
198    let installed: Vec<String> = pacenotes_installer::ledger::Ledger::open(&state.folder)
199        .packages()
200        .keys()
201        .cloned()
202        .collect();
203    state.tree = pacenotes_installer::tree::Tree::build(game, &packages, &cached, &installed);
204    state.packages = packages;
205    // Sized with the model, all zero: a stray notification indexing an empty table aborted the Configure route.
206    state.tree_items = vec![0; state.tree.len()];
207}
208
209/// Every choice category in the tree needs a pick, and a build too old for the
210/// catalogue stops only when something must come down the wire, so it can still
211/// reconfigure and remove; scene 4b does the transfers.
212unsafe fn on_download(dialog: HWND) -> isize {
213    let Some(state) = crate::wizard::page::state(dialog) else {
214        return crate::wizard::page::reply(dialog, 0);
215    };
216    let choices = pacenotes_installer::schema::choice_categories(state.game_id());
217    let missing = state.tree.categories_missing_a_pick(&choices);
218    if !missing.is_empty() {
219        let text = crate::i18n::load_string_arg(crate::i18n::IDS_S4_MISSING, &missing.join(", "));
220        crate::ui::message_box_warning(dialog, &text);
221        return crate::wizard::page::stay(dialog);
222    }
223    if state.schema_mismatch && !state.tree.checked_needing_download().is_empty() {
224        crate::ui::message_box_warning(
225            dialog,
226            &crate::i18n::load_string(crate::i18n::IDS_S4_NEEDS_NEWER),
227        );
228        return crate::wizard::page::stay(dialog);
229    }
230    state.chosen = state.tree.checked_keys();
231    crate::wizard::page::reply(dialog, 0)
232}